Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
70.00% covered (warning)
70.00%
7 / 10
CRAP
87.50% covered (warning)
87.50%
63 / 72
DelegatingProductSaver
0.00% covered (danger)
0.00%
0 / 1
70.00% covered (warning)
70.00%
7 / 10
26.22
87.50% covered (warning)
87.50%
63 / 72
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
12 / 12
 save
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 saveAll
0.00% covered (danger)
0.00%
0 / 1
7.23
83.33% covered (warning)
83.33%
15 / 18
 validateObject
0.00% covered (danger)
0.00%
0 / 1
3.19
33.33% covered (danger)
33.33%
2 / 6
 isOwner
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 canEdit
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getUsername
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 saveProduct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
9 / 9
 saveProductDraft
0.00% covered (danger)
0.00%
0 / 1
4.06
84.62% covered (warning)
84.62%
11 / 13
 getFullProduct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
<?php
/*
 * This file is part of the Akeneo PIM Enterprise Edition.
 *
 * (c) 2014 Akeneo SAS (http://www.akeneo.com)
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace Akeneo\Pim\WorkOrganization\Workflow\Bundle\Doctrine\Common\Saver;
use Akeneo\Pim\Enrichment\Bundle\Doctrine\Common\Saver\ProductUniqueDataSynchronizer;
use Akeneo\Pim\Enrichment\Component\Product\Manager\CompletenessManager;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductInterface;
use Akeneo\Pim\Enrichment\Component\Product\Repository\ProductRepositoryInterface;
use Akeneo\Pim\Permission\Component\Attributes;
use Akeneo\Pim\Permission\Component\NotGrantedDataMergerInterface;
use Akeneo\Pim\WorkOrganization\Workflow\Component\Builder\EntityWithValuesDraftBuilderInterface;
use Akeneo\Pim\WorkOrganization\Workflow\Component\Model\EntityWithValuesDraftInterface;
use Akeneo\Pim\WorkOrganization\Workflow\Component\Repository\EntityWithValuesDraftRepositoryInterface;
use Akeneo\Tool\Component\StorageUtils\Remover\RemoverInterface;
use Akeneo\Tool\Component\StorageUtils\Saver\BulkSaverInterface;
use Akeneo\Tool\Component\StorageUtils\Saver\SaverInterface;
use Akeneo\Tool\Component\StorageUtils\StorageEvents;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\Util\ClassUtils;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
/**
 * Delegating product saver, depending on context it delegates to other savers to deal with drafts or working copies
 *
 * @author Nicolas Dupont <nicolas@akeneo.com>
 */
class DelegatingProductSaver implements SaverInterface, BulkSaverInterface
{
    /** @var CompletenessManager */
    protected $completenessManager;
    /** @var EventDispatcherInterface */
    protected $eventDispatcher;
    /** @var ObjectManager */
    protected $objectManager;
    /** @var AuthorizationCheckerInterface */
    protected $authorizationChecker;
    /** @var EntityWithValuesDraftBuilderInterface */
    protected $entityWithValuesDraftBuilder;
    /** @var TokenStorageInterface */
    protected $tokenStorage;
    /** @var EntityWithValuesDraftRepositoryInterface */
    protected $productDraftRepo;
    /** @var RemoverInterface */
    protected $productDraftRemover;
    /** @var ProductUniqueDataSynchronizer */
    private $uniqueDataSynchronizer;
    /** @var NotGrantedDataMergerInterface */
    private $mergeDataOnProduct;
    /** @var ProductRepositoryInterface */
    private $productRepository;
    /**
     * @param ObjectManager                            $objectManager
     * @param CompletenessManager                      $completenessManager
     * @param EventDispatcherInterface                 $eventDispatcher
     * @param AuthorizationCheckerInterface            $authorizationChecker
     * @param EntityWithValuesDraftBuilderInterface    $entityWithValuesDraftBuilder
     * @param TokenStorageInterface                    $tokenStorage
     * @param EntityWithValuesDraftRepositoryInterface $productDraftRepo
     * @param RemoverInterface                         $productDraftRemover
     * @param ProductUniqueDataSynchronizer            $uniqueDataSynchronizer
     * @param NotGrantedDataMergerInterface            $mergeDataOnProduct
     * @param ProductRepositoryInterface               $productRepository
     */
    public function __construct(
        ObjectManager $objectManager,
        CompletenessManager $completenessManager,
        EventDispatcherInterface $eventDispatcher,
        AuthorizationCheckerInterface $authorizationChecker,
        EntityWithValuesDraftBuilderInterface $entityWithValuesDraftBuilder,
        TokenStorageInterface $tokenStorage,
        EntityWithValuesDraftRepositoryInterface $productDraftRepo,
        RemoverInterface $productDraftRemover,
        ProductUniqueDataSynchronizer $uniqueDataSynchronizer,
        NotGrantedDataMergerInterface $mergeDataOnProduct,
        ProductRepositoryInterface $productRepository
    ) {
        $this->objectManager = $objectManager;
        $this->completenessManager = $completenessManager;
        $this->eventDispatcher = $eventDispatcher;
        $this->authorizationChecker = $authorizationChecker;
        $this->entityWithValuesDraftBuilder = $entityWithValuesDraftBuilder;
        $this->tokenStorage = $tokenStorage;
        $this->productDraftRepo = $productDraftRepo;
        $this->productDraftRemover = $productDraftRemover;
        $this->uniqueDataSynchronizer = $uniqueDataSynchronizer;
        $this->mergeDataOnProduct = $mergeDataOnProduct;
        $this->productRepository = $productRepository;
    }
    /**
     * {@inheritdoc}
     *
     * @throws AuthenticationCredentialsNotFoundException if not authenticated
     */
    public function save($filteredProduct, array $options = [])
    {
        $this->validateObject($filteredProduct, ProductInterface::class);
        $fullProduct = $this->getFullProduct($filteredProduct);
        if ($this->isOwner($fullProduct) || null === $fullProduct->getId()) {
            $this->saveProduct($fullProduct, $options);
        } elseif ($this->canEdit($fullProduct)) {
            $this->saveProductDraft($fullProduct, $options);
        }
    }
    /**
     * {@inheritdoc}
     */
    public function saveAll(array $filteredProducts, array $options = [])
    {
        if (empty($filteredProducts)) {
            return;
        }
        $productsToCompute = [];
        $fullProducts = [];
        foreach ($filteredProducts as $filteredProduct) {
            $this->validateObject($filteredProduct, ProductInterface::class);
            $fullProduct = $this->getFullProduct($filteredProduct);
            $fullProducts[] = $fullProduct;
            if ($this->isOwner($fullProduct) || null === $fullProduct->getId()) {
                $productsToCompute[] = $fullProduct;
                $this->saveProduct($fullProduct, $options, false);
            } elseif ($this->canEdit($fullProduct)) {
                $this->saveProductDraft($fullProduct, $options, false);
            }
        }
        $this->objectManager->flush();
        foreach ($productsToCompute as $product) {
            $this->eventDispatcher->dispatch(StorageEvents::POST_SAVE, new GenericEvent($product, $options));
        }
        $this->eventDispatcher->dispatch(StorageEvents::POST_SAVE_ALL, new GenericEvent($fullProducts, $options));
    }
    /**
     * Raises an exception when we try to save another object than expected
     *
     * @param object $object
     * @param string $expectedClass
     *
     * @throws \InvalidArgumentException
     */
    protected function validateObject($object, $expectedClass)
    {
        if (!$object instanceof $expectedClass) {
            throw new \InvalidArgumentException(
                sprintf(
                    'Expects a %s, "%s" provided',
                    $expectedClass,
                    ClassUtils::getClass($object)
                )
            );
        }
    }
    /**
     * Is user owner of the product?
     *
     * @param ProductInterface $product
     *
     * @return bool
     */
    protected function isOwner(ProductInterface $product)
    {
        return $this->authorizationChecker->isGranted(Attributes::OWN, $product);
    }
    /**
     * Can user edit the product?
     *
     * @param ProductInterface $product
     *
     * @return bool
     */
    protected function canEdit(ProductInterface $product)
    {
        return $this->authorizationChecker->isGranted(Attributes::EDIT, $product);
    }
    /**
     * @return string
     */
    protected function getUsername()
    {
        return $this->tokenStorage->getToken()->getUser()->getUsername();
    }
    /**
     * @param ProductInterface $fullProduct
     * @param array            $options
     * @param bool|true        $withFlush
     */
    protected function saveProduct(ProductInterface $fullProduct, array $options, $withFlush = true)
    {
        $options['unitary'] = true;
        $this->eventDispatcher->dispatch(StorageEvents::PRE_SAVE, new GenericEvent($fullProduct, $options));
        $this->completenessManager->generateMissingForProduct($fullProduct);
        $this->uniqueDataSynchronizer->synchronize($fullProduct);
        $this->objectManager->persist($fullProduct);
        if ($withFlush) {
            $this->objectManager->flush();
            $this->eventDispatcher->dispatch(StorageEvents::POST_SAVE, new GenericEvent($fullProduct, $options));
        }
    }
    /**
     * @param ProductInterface $fullProduct
     * @param array            $options
     * @param bool|true        $withFlush
     */
    protected function saveProductDraft(ProductInterface $fullProduct, array $options, $withFlush = true)
    {
        $productDraft = $this->entityWithValuesDraftBuilder->build($fullProduct, $this->getUsername());
        if (null !== $productDraft) {
            $this->validateObject($productDraft, EntityWithValuesDraftInterface::class);
            $options['unitary'] = true;
            $this->eventDispatcher->dispatch(StorageEvents::PRE_SAVE, new GenericEvent($productDraft, $options));
            $this->objectManager->persist($productDraft);
            if ($withFlush) {
                $this->objectManager->flush();
                $this->eventDispatcher->dispatch(StorageEvents::POST_SAVE, new GenericEvent($productDraft, $options));
                $this->objectManager->refresh($fullProduct);
            }
        } elseif (null !== $draft = $this->productDraftRepo->findUserEntityWithValuesDraft($fullProduct, $this->getUsername())) {
            $this->productDraftRemover->remove($draft);
        }
    }
    /**
     * $filteredProduct is the product with only granted data.
     * To avoid to lose data, we have to send to the save the full product with all data (included not granted).
     * To do that, we get the product from the DB and merge new data from $filteredProduct into this product.
     *
     * @param ProductInterface $filteredProduct
     *
     * @return ProductInterface
     */
    private function getFullProduct(ProductInterface $filteredProduct): ProductInterface
    {
        if (null === $filteredProduct->getId()) {
            return $this->mergeDataOnProduct->merge($filteredProduct);
        }
        $fullProduct = $this->productRepository->find($filteredProduct->getId());
        return $this->mergeDataOnProduct->merge($filteredProduct, $fullProduct);
    }
}